import { dom, EventHandler, Nodes } from '@knno/dom';
import { FormItem } from '../interface';
import { tagStyle } from './tag.css';
import { takeIf } from '../../tools/sugar';
import SVGClose from '@material-design-icons/svg/filled/close.svg';
import { PopList, SelectHandler } from '../popList/popList';
import { Validatable } from '../validatable';
import { DATA } from '../symbol';

export interface TagEventMap {
	prefixiconclick: Event;
}
export type TagEventKey = keyof TagEventMap;
export type TagEventHandler<T extends TagEventKey, O extends Nodes> = (this: O, event: TagEventMap[T]) => void;
export type TagValue = string | { value: unknown; text?: string };

export interface Tag {
	on<K extends keyof TagEventMap>(
		event: K,
		listener: TagEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	on<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof TagEventMap>(
		event: K,
		listener: TagEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
}

function getValue(o: TagValue): unknown {
	if (typeof o === 'string') return o;
	return o.value;
}

function getText(o: TagValue): string {
	if (typeof o === 'string') return o;
	return o.text ?? '';
}

export interface TagPopupHandler<T extends Nodes> {
	(this: T, values: TagValue[]): Promise<TagValue[]>;
}

export class Tag extends Validatable implements FormItem {
	private [DATA]: {
		arr: TagValue[];
		opts: TagValue[];
		input: Nodes;
		pop: SelectHandler | undefined;
		ph: string;
		handler: TagPopupHandler<Tag> | null;
	};

	constructor() {
		const input = dom.input();
		const root = dom
			.div(tagStyle)
			.append(input)
			.on('mousedown', () => {
				setTimeout(() => {
					input.focus();
				});
			});
		super(root);
		this[DATA] = {
			arr: [],
			opts: [],
			input,
			ph: '',
			pop: undefined,
			handler: null,
		};

		this[DATA].input = input;
		root.on('keydown', (e) => {
			if (!this.selectOnly()) return;
			if (e.key === 'Backspace') {
				if (this[DATA].arr.length > 0) {
					this[DATA].arr.pop();
					this.showPlaceHolder();
					this.invalid(false);
					const div = input.elems()[0].previousElementSibling;
					if (div) {
						this.remove(div);
						e.preventDefault();
						this.emit(new Event('change'));
					}
				}
			} else if (e.key === 'ArrowDown') {
				if (this[DATA].pop) {
					input.value('');
					this[DATA].pop.list.focus();
					this[DATA].pop.list.active(0);
					e.preventDefault();
				} else {
					this.openSelect();
					setTimeout(() => {
						this[DATA].pop?.list?.focus();
						this[DATA].pop?.list?.active(0);
					});
				}
			}
		});
		input.on('input', () => {
			this.invalid(false);
		});
		input.on('change', () => {
			this.invalid(false);
		});
		input.on('focus', () => {
			this.addClass('focus');
		});
		const addValue = () => {
			const v = input.value() as string;
			if (v) {
				input.value('');
				const max = this.max();
				if (max === null || this[DATA].arr.length < max) {
					this[DATA].arr.push(v);
					this.showPlaceHolder();
					this.addMark(v);
					this.emit(new Event('change'));
				}
			}
		};
		input.on('blur', () => {
			addValue();
			this.removeClass('focus');
		});
		input.on('keypress', (evt) => {
			if (evt.key === 'Enter') {
				this.invalid(false);
				addValue();
			}
		});
		let openTimer: NodeJS.Timeout | null = null;
		input.on('keyup', () => {
			if (this.selectOnly()) return;
			if (this[DATA].opts.length === 0) return;
			if (openTimer) clearTimeout(openTimer);
			openTimer = setTimeout(() => {
				this.openSelect();
			}, 32);
		});
		this.on('click', () => {
			this.selectOnly() && this.openSelect();
		});
		input.on('dblclick', () => {
			this.openSelect();
		});
		input.on('keydown', (e) => {
			if (e.key === 'Backspace') {
				if (!input.value()) {
					if (this[DATA].arr.length > 0) {
						this[DATA].arr.pop();
						this.showPlaceHolder();
						this.invalid(false);
						const div = input.elems()[0].previousElementSibling;
						if (div) {
							this.remove(div);
							e.stopPropagation();
							e.preventDefault();
							this.emit(new Event('change'));
						}
					}
				}
			} else if (e.key === 'ArrowDown') {
				if (this[DATA].pop) {
					input.value('');
					this[DATA].pop.list.focus();
					this[DATA].pop.list.active(0);
					e.preventDefault();
				} else {
					if (this[DATA].opts.length > 0) {
						this.openSelect();
					}
				}
			} else if (e.key !== 'Tab') {
				const max = this.max();
				if (max != null && this[DATA].arr.length >= max) {
					e.preventDefault();
					return;
				}
			}
		});
	}

	private addMark(text: string) {
		const div = dom.div().append(
			dom.span().text(text),
			dom.html(SVGClose).on('mousedown', (e) => {
				const idx = this.query(':scope>div').elems().indexOf(div.elems()[0]);
				div.detach();
				this[DATA].arr.splice(idx, 1);
				this.showPlaceHolder();
				e.preventDefault();
				setTimeout(() => {
					this.focus();
				});
				this.emit(new Event('change'));
			})
		);

		div.insertTo(this.elems(), this[DATA].input);
	}

	clear(): this {
		this.value([]);
		return this;
	}

	disabled(): boolean;
	disabled(value: boolean): this;
	disabled(value?: boolean): boolean | this {
		if (value === undefined) {
			return this.attr('disabled') != null;
		}
		this.attr('disabled', value);
		this[DATA].input.attr('disabled', value);
		return this;
	}

	readonly(): boolean;
	readonly(value: boolean): this;
	readonly(value?: boolean): boolean | this {
		if (value === undefined) {
			return this.attr('readonly') != null;
		}
		this.attr('readonly', value);
		this[DATA].input.attr('readonly', value);
		return this;
	}

	max(): number | null;
	max(len: number | null): this;
	max(len?: number | null): this | number | null {
		if (len !== undefined) {
			this.attr('max', len ? len + '' : null);
			return this;
		}
		return takeIf(this.attr('max'), (v) => parseInt(v)) ?? null;
	}

	prefixIcon(icon: string | null): this {
		const svg = this.query('svg[name=prefix]');
		this.remove(svg);
		if (icon) {
			this.insert(
				dom
					.html(icon)
					.name('prefix')
					.on('click', () => {
						if (this.disabled()) return;
						const evt = new Event('prefixiconclick');
						this.emit(evt);
					})
			);
		}
		return this;
	}

	override text(): string;
	override text(text: string): this;
	override text(text?: string): this | string {
		if (typeof text === 'string') {
			return this;
		}
		return this[DATA].arr
			.map((i) => (typeof i === 'string' ? i : typeof i === 'object' ? i.text ?? i.value + '' : i + ''))
			.join(', ');
	}

	textArr(): string[] {
		return this[DATA].arr.map((i) =>
			typeof i === 'string' ? i : typeof i === 'object' ? i.text ?? i.value + '' : i + ''
		);
	}

	valueArr(): unknown[] {
		return this[DATA].arr.map((i) => (typeof i === 'object' ? i.value : i));
	}

	override value(): TagValue[];
	override value(values: TagValue[]): this;
	override value(values?: TagValue[]): this | TagValue[] {
		if (values instanceof Array) {
			this[DATA].arr = [...values];
			const divs = this.query(':scope>div');
			divs.detach();
			values.forEach((v) => {
				let s: string;
				if (typeof v === 'string') {
					s = v;
				} else if (typeof v === 'object') {
					s = v.text ?? v.value + '';
				} else {
					s = v + '';
				}
				this.addMark(s);
				this[DATA].input.value('');
			});
			this.showPlaceHolder();
			return this;
		}
		return this[DATA].arr;
	}

	override focus(options?: FocusOptions | undefined): this {
		if (this.selectOnly()) {
			super.focus(options);
		} else {
			this[DATA].input.focus(options);
		}
		return this;
	}

	private showPlaceHolder() {
		if (this.value().length <= 0) {
			this[DATA].input.prop('placeholder', this[DATA].ph);
		} else {
			this[DATA].input.prop('placeholder', '');
		}
	}

	placeholder(): string;
	placeholder(text: string): this;
	placeholder(text?: string): this | string | undefined {
		if (typeof text === 'string') {
			this[DATA].ph = text;
			this.showPlaceHolder();
			return this;
		}
		return this[DATA].input.prop('placeholder') as string;
	}

	maxLength(): number | null;
	maxLength(len: number | null): this;
	maxLength(len?: number | null): this | number | null {
		if (len !== undefined) {
			this[DATA].input.attr('maxlength', len ? len + '' : null);
			return this;
		}
		return takeIf(this[DATA].input.attr('maxlength'), (v) => parseInt(v)) ?? null;
	}

	options(): TagValue[];
	options(options: TagValue[]): this;
	options(options?: TagValue[]): this | TagValue[] {
		if (options === undefined) {
			return this[DATA].opts;
		}
		if (options instanceof Array) {
			this[DATA].opts = options;
		}
		return this;
	}

	selectOnly(): boolean;
	selectOnly(value: boolean): this;
	selectOnly(value?: boolean): boolean | this {
		if (value === undefined) {
			return this.attr('select-only') != null;
		}
		this.attr('select-only', !!value);
		this[DATA].input.attr('readonly', value);
		if (value) {
			this.attr('tabindex', '0');
		} else {
			this.attr('tabindex', null);
		}
		return this;
	}

	onPopup(handler: TagPopupHandler<Tag> | null) {
		this[DATA].handler = handler;
		return this;
	}

	private openSelect() {
		const max = this.max();
		if (this.disabled() || this.readonly() || (max != null && this[DATA].arr.length >= max)) return;
		const values = this.value();
		if (this[DATA].handler) {
			const fn = this[DATA].handler.bind(this);
			fn(values)
				.then((result) => {
					if (result) {
						this.value(result);
						this.invalid(false);
						this.emit(new Event('change'));
					}
					setTimeout(() => {
						this.focus();
					});
				})
				.catch((reason) => {
					console.error(reason);
				});
			return;
		}
		const options = this[DATA].opts
			.filter((o) => !values.find((v) => getValue(v) === getValue(o)))
			.filter((o) => {
				const t = this[DATA].input.value();
				return getText(o).includes(t + '');
			});
		if (options.length === 0) {
			this[DATA].pop?.close();
			return;
		}
		if (this[DATA].pop) {
			this[DATA].pop.list.rows(options);
			return;
		}
		this[DATA].pop = PopList.show(
			options,
			{
				text: 'text',
				value: 'value',
			},
			{
				refer: this,
				activeValue: null,
				multiSelect: false,
				useAction: true,
				useSearch: false,
				focus: false,
			}
		)
			.onSelect((value, text) => {
				this.value([...values, { value, text: text + '' }]);
				setTimeout(() => {
					this.focus();
				});
				this.invalid(false);
				this.emit(new Event('change'));
			})
			.onClose(() => {
				delete this[DATA].pop;
			});
	}
}

export function tag(...className: string[]): Tag {
	return new Tag().addClass(...className);
}
